/*
 * Copyright (c) 2009, 2012 IBM Corp.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Dave Locke - initial API and implementation and/or initial documentation
 */
package org.eclipse.paho.client.mqttv3.persist;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Vector;

import org.eclipse.paho.client.mqttv3.MqttClientPersistence;
import org.eclipse.paho.client.mqttv3.MqttPersistable;
import org.eclipse.paho.client.mqttv3.MqttPersistenceException;
import org.eclipse.paho.client.mqttv3.internal.FileLock;
import org.eclipse.paho.client.mqttv3.internal.MqttPersistentData;

/**
 * An implementation of the {@link MqttClientPersistence} interface that provides file based persistence.
 *
 * A directory is specified when the Persistence object is created. When the persistence is then opened (see {@link #open(String, String)}), a sub-directory is made beneath the
 * base for this client ID and connection key. This allows one persistence base directory to be shared by multiple clients.
 *
 * The sub-directory's name is created from a concatenation of the client ID and connection key with any instance of '/', '\\', ':' or ' ' removed.
 */
public class MqttDefaultFilePersistence implements MqttClientPersistence {

    private File dataDir;
    private File clientDir = null;
    private FileLock fileLock = null;
    private static final String MESSAGE_FILE_EXTENSION = ".msg";
    private static final String MESSAGE_BACKUP_FILE_EXTENSION = ".bup";
    private static final String LOCK_FILENAME = ".lck";

    private static final FilenameFilter FILE_FILTER = new FilenameFilter() {
        public boolean accept(File dir, String name) {
            return name.endsWith(MESSAGE_FILE_EXTENSION);
        }
    };

    public MqttDefaultFilePersistence() { //throws MqttPersistenceException {
        this(System.getProperty("user.dir"));
    }

    /**
     * Create an file-based persistent data store within the specified directory.
     *
     * @param directory the directory to use.
     */
    public MqttDefaultFilePersistence(String directory) { //throws MqttPersistenceException {
        dataDir = new File(directory);
    }

    public void open(String clientId, String theConnection) throws MqttPersistenceException {

        if (dataDir.exists() && !dataDir.isDirectory()) {
            throw new MqttPersistenceException();
        } else if (!dataDir.exists()) {
            if (!dataDir.mkdirs()) {
                throw new MqttPersistenceException();
            }
        }
        if (!dataDir.canWrite()) {
            throw new MqttPersistenceException();
        }

        StringBuffer keyBuffer = new StringBuffer();
        for (int i = 0; i < clientId.length(); i++) {
            char c = clientId.charAt(i);
            if (isSafeChar(c)) {
                keyBuffer.append(c);
            }
        }
        keyBuffer.append("-");
        for (int i = 0; i < theConnection.length(); i++) {
            char c = theConnection.charAt(i);
            if (isSafeChar(c)) {
                keyBuffer.append(c);
            }
        }
        String key = keyBuffer.toString();

        clientDir = new File(dataDir, key);

        if (!clientDir.exists()) {
            clientDir.mkdir();
        }

        try {
            fileLock = new FileLock(clientDir, LOCK_FILENAME);
        } catch (Exception e) {
            throw new MqttPersistenceException(MqttPersistenceException.REASON_CODE_PERSISTENCE_IN_USE);
        }

        // Scan the directory for .backup files. These will
        // still exist if the JVM exited during addMessage, before
        // the new message was written to disk and the backup removed.
        restoreBackups(clientDir);

    }

    /**
     * Checks whether the persistence has been opened.
     *
     * @throws MqttPersistenceException if the persistence has not been opened.
     */
    private void checkIsOpen() throws MqttPersistenceException {
        if (clientDir == null) {
            throw new MqttPersistenceException();
        }
    }

    public void close() throws MqttPersistenceException {

//		checkIsOpen();
        if (fileLock != null) {
            fileLock.release();
        }

        if (getFiles().length == 0) {
            clientDir.delete();
        }
        clientDir = null;
    }

    /**
     * Writes the specified persistent data to the previously specified persistence directory. This method uses a safe overwrite policy to ensure IO errors do not lose messages.
     *
     * @param message
     * @throws MqttPersistenceException
     */
    public void put(String key, MqttPersistable message) throws MqttPersistenceException {
        checkIsOpen();
        File file = new File(clientDir, key + MESSAGE_FILE_EXTENSION);
        File backupFile = new File(clientDir, key + MESSAGE_FILE_EXTENSION + MESSAGE_BACKUP_FILE_EXTENSION);

        if (file.exists()) {
            // Backup the existing file so the overwrite can be rolled-back
            boolean result = file.renameTo(backupFile);
            if (!result) {
                backupFile.delete();
                file.renameTo(backupFile);
            }
        }
        try {
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(message.getHeaderBytes(), message.getHeaderOffset(), message.getHeaderLength());
            if (message.getPayloadBytes() != null) {
                fos.write(message.getPayloadBytes(), message.getPayloadOffset(), message.getPayloadLength());
            }
            fos.getFD().sync();
            fos.close();
            if (backupFile.exists()) {
                // The write has completed successfully, delete the backup
                backupFile.delete();
            }
        } catch (IOException ex) {
            throw new MqttPersistenceException(ex);
        } finally {
            if (backupFile.exists()) {
                // The write has failed - restore the backup
                boolean result = backupFile.renameTo(file);
                if (!result) {
                    file.delete();
                    backupFile.renameTo(file);
                }
            }
        }
    }

    public MqttPersistable get(String key) throws MqttPersistenceException {
        checkIsOpen();
        MqttPersistable result;
        try {
            File file = new File(clientDir, key + MESSAGE_FILE_EXTENSION);
            FileInputStream fis = new FileInputStream(file);
            int size = fis.available();
            byte[] data = new byte[size];
            int read = 0;
            while (read < size) {
                read += fis.read(data, read, size - read);
            }
            fis.close();
            result = new MqttPersistentData(key, data, 0, data.length, null, 0, 0);
        } catch (IOException ex) {
            throw new MqttPersistenceException(ex);
        }
        return result;
    }

    /**
     * Deletes the data with the specified key from the previously specified persistence directory.
     */
    public void remove(String key) throws MqttPersistenceException {
        checkIsOpen();
        File file = new File(clientDir, key + MESSAGE_FILE_EXTENSION);
        if (file.exists()) {
            file.delete();
        }
    }

    /**
     * Returns all of the persistent data from the previously specified persistence directory.
     *
     * @return all of the persistent data from the persistence directory.
     * @throws MqttPersistenceException
     */
    public Enumeration keys() throws MqttPersistenceException {
        checkIsOpen();
        File[] files = getFiles();
        Vector result = new Vector(files.length);
        for (int i = 0; i < files.length; i++) {
            String filename = files[i].getName();
            String key = filename.substring(0, filename.length() - MESSAGE_FILE_EXTENSION.length());
            result.addElement(key);
        }
        return result.elements();
    }

    private File[] getFiles() throws MqttPersistenceException {
        checkIsOpen();
        File[] files = clientDir.listFiles(FILE_FILTER);
        if (files == null) {
            throw new MqttPersistenceException();
        }
        return files;
    }

    private boolean isSafeChar(char c) {
        return Character.isJavaIdentifierPart(c) || c == '-';
    }

    /**
     * Identifies any backup files in the specified directory and restores them to their original file. This will overwrite any existing file of the same name. This is safe as a
     * stray backup file will only exist if a problem occured whilst writing to the original file.
     *
     * @param dir The directory in which to scan and restore backups
     */
    private void restoreBackups(File dir) throws MqttPersistenceException {
        File[] files = dir.listFiles(new FileFilter() {
            public boolean accept(File f) {
                return f.getName().endsWith(MESSAGE_BACKUP_FILE_EXTENSION);
            }
        });
        if (files == null) {
            throw new MqttPersistenceException();
        }

        for (int i = 0; i < files.length; i++) {
            File originalFile = new File(dir, files[i].getName().substring(0, files[i].getName().length() - MESSAGE_BACKUP_FILE_EXTENSION.length()));
            boolean result = files[i].renameTo(originalFile);
            if (!result) {
                originalFile.delete();
                files[i].renameTo(originalFile);
            }
        }
    }

    public boolean containsKey(String key) throws MqttPersistenceException {
        checkIsOpen();
        File file = new File(clientDir, key + MESSAGE_FILE_EXTENSION);
        return file.exists();
    }

    public void clear() throws MqttPersistenceException {
        checkIsOpen();
        File[] files = getFiles();
        for (int i = 0; i < files.length; i++) {
            files[i].delete();
        }
    }
}
